﻿#region License
/*
 * Spec2XML - Converts *.spec and *.tm files to an XML format.
 * Copyright (C) 2008 Rodger Aiken <null.coded@gmail.com>
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 
 * General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>. 
 */
#endregion

namespace CodePoints.Spec2Xml
{
	using System;
	using System.Collections.Generic;
	using System.Globalization;
	using System.IO;
	using System.Net;
	using System.Reflection;
	using System.Text.RegularExpressions;
	using System.Xml;

	public static class Spec2Xml
	{
		#region Patterns

		private static readonly Regex [ ] _patterns = new Regex [ ] {
			new Regex(
				@"^(([\-\._0-9a-zA-Z]+)://)",
				( RegexOptions.CultureInvariant | RegexOptions.Singleline )
			),
			new Regex(
				@"^(([\-\._0-9a-zA-Z]+)://)?([\-0-9a-zA-Z]+((\.[\-0-9a-zA-Z]+)*))((/(.*))?)$",
				( RegexOptions.CultureInvariant | RegexOptions.Singleline )
			),
			new Regex(
				@"^([_a-zA-Z][_0-9a-zA-Z]*):\s+(.*)",
				( RegexOptions.CultureInvariant | RegexOptions.Multiline )
			),
			new Regex(
				@"^([_a-zA-Z][_0-9a-zA-Z]*)\s*\((.*)?\)$",
				( RegexOptions.CultureInvariant | RegexOptions.Multiline )
			)
		};

		#endregion

		// Get file data whether local or URL
		private static string GetFile ( string file ) {
			try {
				if ( _patterns [ 1 ].IsMatch(file) ) {
					if ( !_patterns [ 0 ].IsMatch(file) )
						file = ( "http://" + file );

					WebRequest request = WebRequest.CreateDefault(new Uri(file));

					using ( WebResponse response = request.GetResponse() ) {
						using ( Stream stream = response.GetResponseStream() ) {
							using ( StreamReader reader = new StreamReader(stream, true) )
								return reader.ReadToEnd();
						}
					}
				} else {
					return File.ReadAllText(file);
				}
			} catch ( Exception exception ) {
				throw new FileNotFoundException("Unable to download file", file, exception);
			}
		}

		// Get file data whether local or URL and separated into lines.
		private static string [ ] GetFileLines ( string file ) {
			try {
				if ( _patterns [ 1 ].IsMatch(file) ) {
					if ( !_patterns [ 0 ].IsMatch(file) )
						file = ( "http://" + file );

					WebRequest request = WebRequest.CreateDefault(new Uri(file));
					List<string> lines = new List<string>();

					using ( WebResponse response = request.GetResponse() ) {
						using ( Stream stream = response.GetResponseStream() ) {
							using ( StreamReader reader = new StreamReader(stream, true) ) {
								string s = null;

								while ( ( s = reader.ReadLine() ) != null )
									lines.Add(s);

								return lines.ToArray();
							}
						}
					}
				} else {
					return File.ReadAllLines(file);
				}
			} catch ( Exception exception ) {
				throw new FileNotFoundException(string.Format("Unable to download file {0}", file), file, exception);
			}
		}

		// Fetch an alias element by its name attribute
		// NOTE: Using the SelectNode(s) method with an XPath expression would most
		// likely be faster.
		private static XmlElement GetAliasByName ( XmlDocument document, string name ) {
			XmlNodeList nodeList = document.GetElementsByTagName("alias");

			if ( nodeList.Count > 0 ) {
				for ( int n = 0 ; n < nodeList.Count ; n++ ) {
					if ( nodeList [ n ].Attributes [ "name" ].Value.Equals(name, StringComparison.InvariantCultureIgnoreCase) )
						return ( ( XmlElement ) nodeList [ n ] );
				}
			}

			return null;
		}

		// Append enumerant to a named type alias in a set of type-mapping XML documents.
		private static void AppendAliasEnums ( XmlDocument [ ] typeXmlFiles, string alias, string name ) {
			XmlElement aliasElement = null, enumElement = null;

			for ( int x = 0 ; x < typeXmlFiles.Length ; x++ ) {
				if ( ( aliasElement = GetAliasByName(typeXmlFiles [ x ], alias) ) != null ) {
					enumElement = typeXmlFiles [ x ].CreateElement("enum");

					enumElement.SetAttribute("name", name);
					aliasElement.AppendChild(enumElement);
				}
			}
		}

		// Create an enum element, and parse enum*.spec character data
		private static void CreateEnum ( XmlDocument document, string [ ] cdata, XmlDocument [ ] typeXmlFiles ) {
			string target = null; string [ ] split = null;
			XmlElement enumElement = null, declElement = null;

			for ( int n = 0 ; n < cdata.Length ; n++ ) {
				// Skip current line if empty, if its a comment, or its flagged as 'passthru'
				if ( ( cdata [ n ].Length < 1 ) || ( cdata [ n ] [ 0 ] == '#' ) || cdata [ n ].StartsWith("passthru:") )
					continue;

				// Transform line, replace tabs and double-spaces with single space.
				cdata [ n ] = cdata [ n ].Replace('\t', ' ');
				cdata [ n ] = cdata [ n ].Replace("  ", " ");
				split = cdata [ n ].Split(new char [ ] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

				if ( split.Length > 2 ) {
					if ( split [ 0 ] == "use" ) {
						// Add enumerants to type alias
						if ( target != null )
							AppendAliasEnums(typeXmlFiles, target, split [ 2 ]);
					} else if ( split [ 1 ] [ 0 ] == '=' ) {
						declElement = document.CreateElement("decl");

						declElement.SetAttribute("name", split [ 0 ]);

						// Change hexa-decimal values to base-10 integers
						if ( split [ 2 ].StartsWith("0x") ) {
							split [ 2 ] = split [ 2 ].Remove(0, 2);
							split [ 2 ] = ( Int64.Parse(split [ 2 ], NumberStyles.HexNumber) ).ToString();
						}

						declElement.SetAttribute("value", split [ 2 ]);

						if ( enumElement == null )
							enumElement = document.CreateElement("enum");

						enumElement.AppendChild(declElement);

						// Add enumerants to type alias
						if ( target != null )
							AppendAliasEnums(typeXmlFiles, target, split [ 0 ]);
					}
				} else if ( ( split.Length > 1 ) && ( split [ 1 ] == "enum:" ) ) {
					// Change enumerant target
					target = split [ 0 ];
				}
			}

			// If enum element created then append it to an XML document.
			if ( enumElement != null )
				document.DocumentElement.AppendChild(enumElement);
		}

		// Create decl element from parsing property character data
		private static void CreateDecl ( XmlDocument document, string cdata ) {
			cdata = cdata.Replace('\t', ' ');
			cdata = cdata.Replace("  ", " ");
			string [ ] split = cdata.Split(new string [ ] { ": " }, 2, StringSplitOptions.RemoveEmptyEntries);

			if ( split [ 0 ].ToLower() != "newcategory" ) {
				XmlElement defElement = document.CreateElement("decl");

				// If the rule targets the param property
				// change it to param@flags
				if ( split [ 0 ] == "param" )
					split [ 0 ] += "@flags";

				defElement.SetAttribute("name", split [ 0 ]);

				if ( ( split.Length > 1 ) && ( split [ 1 ] != "*" ) )
					defElement.SetAttribute("values", split [ 1 ].Replace(" ", ","));

				document.DocumentElement.AppendChild(defElement);
			}
		}

		// Create func element from parsing function character data
		private static void CreateFunc ( XmlDocument document, string [ ] cdata, string identifier ) {
			XmlElement funcElement = document.CreateElement("func");

			// Set both the id attribute and name attribute so that
			// GetElementById can be used.
			funcElement.SetAttribute("id", identifier);
			funcElement.SetAttribute("name", identifier);

			XmlElement paramElement = null, attrElement = null;
			string [ ] split = null;

			for ( int n = 0 ; n < cdata.Length ; n++ ) {
				// Skip current line if empty, if its a comment, or its flagged as 'passthru'
				if ( ( cdata [ n ].Length < 1 ) || ( cdata [ n ] [ 0 ] == '#' ) || cdata [ n ].StartsWith("passthru:") )
					continue;

				cdata [ n ] = cdata [ n ].Replace('\t', ' ');
				cdata [ n ] = cdata [ n ].Replace("  ", " ");
				split = cdata [ n ].Split(new char [ ] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

				if ( ( split.Length < 2 ) || ( split [ 1 ] == "?" ) )
					continue;

				switch ( split [ 0 ].ToLower() ) {
					case "return":
						if ( split [ 1 ].ToLower() != "void" )
							funcElement.SetAttribute("return", split [ 1 ]);

						break;
					case "param":
						paramElement = document.CreateElement("param");

						paramElement.SetAttribute("name", split [ 1 ]);
						paramElement.SetAttribute("type", split [ 2 ]);
						paramElement.SetAttribute("direction", split [ 3 ]);
						paramElement.SetAttribute("class", split [ 4 ]);

						if ( split.Length > 5 ) {
							if ( split [ 4 ] == "array" ) {
								split [ 5 ] = split [ 5 ].Substring(1, ( split [ 5 ].Length - 2 ));
								split [ 5 ] = split [ 5 ].Trim();

								if ( split [ 5 ].Length > 0 ) {
									split [ 5 ] = split [ 5 ].Replace("COMPSIZE", "cmp");
									split [ 5 ] = split [ 5 ].Replace('/', ',');

									paramElement.SetAttribute("size", split [ 5 ]);
								}
							}

							if ( split.Length > 6 ) {
								string flags = string.Join(" ", split, 6, ( split.Length - 6 ));
								flags = flags.Replace(' ', ',');

								paramElement.SetAttribute("flags", flags);
							}
						}

						funcElement.AppendChild(paramElement);
						break;
					default:
						if ( ( split.Length == 2 ) && split [ 1 ].StartsWith("0x") ) {
							split [ 1 ] = split [ 1 ].Remove(0, 2);
							split [ 1 ] = Int64.Parse(split [ 1 ], NumberStyles.HexNumber).ToString();
						}

						string value = string.Join(" ", split, 1, ( split.Length - 1 ));
						value = value.Replace(' ', ',');
						attrElement = document.CreateElement("attr");

						attrElement.SetAttribute("name", split [ 0 ]);
						attrElement.SetAttribute("value", value);
						funcElement.AppendChild(attrElement);
						break;
				}
			}

			// Append func element to spec element
			document.DocumentElement.AppendChild(funcElement);
		}

		// Create typemap element from parsing tpemap character data
		private static void CreateTypeMap ( XmlDocument document, string [ ] cdata ) {
			XmlElement typemapElement = null, aliasElement = null;
			string [ ] split = null;

			for ( int n = 0 ; n < cdata.Length ; n++ ) {
				// Skip current line if empty, if its a comment, or its flagged as 'passthru'
				if ( ( cdata [ n ].Length < 1 ) || ( cdata [ n ] [ 0 ] == '#' ) || cdata [ n ].StartsWith("passthru:") )
					continue;

				cdata [ n ] = cdata [ n ].Replace('\t', ' ');
				cdata [ n ] = cdata [ n ].Replace("  ", " ");
				split = cdata [ n ].Split(new char [ ] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);

				if ( split.Length > 5 ) {
					aliasElement = document.CreateElement("alias");

					aliasElement.SetAttribute("name", split [ 0 ]);
					aliasElement.SetAttribute("type", split [ 3 ]);

					if ( typemapElement == null )
						typemapElement = document.CreateElement("typemap");

					typemapElement.AppendChild(aliasElement);
				}
			}

			// Append typemap element to spec element if any types are defined
			if ( typemapElement != null )
				document.DocumentElement.AppendChild(typemapElement);
		}

		// Create XmlTextWriter that uses tabs instead of spaces
		private static XmlTextWriter CreateXmlTextWriter ( string file ) {
			XmlTextWriter textWriter = new XmlTextWriter(new StreamWriter(file));
			textWriter.Indentation = 1;
			textWriter.IndentChar = '\t';
			textWriter.Formatting = Formatting.Indented;
			return textWriter;
		}

		// Show usage information
		private static void Usage ( string application ) {
			Console.WriteLine();
			Console.WriteLine("usage: {0} (<specification>|-(h|v)) [-(E|h|o|s|t|V|v) [argument]]", application);
			Console.WriteLine();
			Console.WriteLine("  specification : URL or file path");
			Console.WriteLine();
			Console.WriteLine("[Options]");
			Console.WriteLine("  -t <typemaps> : *.tm file(s) URL or paths");
			Console.WriteLine("  -E <enums>    : enum*.spec file(s) URL or paths");
			Console.WriteLine("  -s            : Split output into separate files.");
			Console.WriteLine("  -o <filename> : File path for single output.");
			Console.WriteLine("  -h            : Display this help information");
			Console.WriteLine("  -v            : Display version information");
			Console.WriteLine("  -V            : Turn on verbosity");
		}

		// Show program information
		private static void About ( ) {
			Assembly assembly = typeof(Spec2Xml).Assembly;

			Console.WriteLine();
			// Show product name and version
			Console.WriteLine("{0} {1} - {2}", ( assembly.GetCustomAttributes(typeof(AssemblyProductAttribute), false) [ 0 ] as AssemblyProductAttribute ).Product, assembly.GetName(false).Version.ToString(3), ( assembly.GetCustomAttributes(typeof(AssemblyTrademarkAttribute), false) [ 0 ] as AssemblyTrademarkAttribute ).Trademark);
			// Show copyright information
			Console.WriteLine(( assembly.GetCustomAttributes(typeof(AssemblyCopyrightAttribute), false) [ 0 ] as AssemblyCopyrightAttribute ).Copyright);
			Console.WriteLine();
			// Show program description
			Console.WriteLine(( assembly.GetCustomAttributes(typeof(AssemblyDescriptionAttribute), false) [ 0 ] as AssemblyDescriptionAttribute ).Description);
		}

		public static void Main ( string [ ] args ) {
			try {
				if ( args.Length < 1 ) {
					Console.WriteLine("error: insufficient arguments");
					Usage(Path.GetFileName(Environment.GetCommandLineArgs() [ 0 ]));
					return;
				}

				string [ ] specFiles = new string [ ] { args [ 0 ] };
				string [ ] typeFiles = null;
				string [ ] enumFiles = null;
				bool splitFiles = false, verbose = false, genFileName = true;
				string outFileName = "output";

				#region parse command-line options (Unix-style)

				if ( args.Length < 2 ) {
					// Show help or version information only
					for ( int n = 0, c = ( -1 ) ; n < args.Length ; n++ ) {
						if ( args [ n ] [ 0 ] == '-' ) {
							if ( args [ n ].Length < 2 ) {
								Console.WriteLine("error: invalid arguments");
								Usage(Path.GetFileName(Environment.GetCommandLineArgs() [ 0 ]));
								return;
							}

							for ( c = 1 ; c < args [ n ].Length ; c++ ) {
								switch ( args [ n ] [ c ] ) {
									case 'h':
										Usage(Path.GetFileName(Environment.GetCommandLineArgs() [ 0 ]));
										break;
									case 'v':
										About();
										break;
									default:
										Console.WriteLine("error: invalid options");
										Usage(Path.GetFileName(Environment.GetCommandLineArgs() [ 0 ]));
										return;
								}
							}
						}
					}

					return;
				} else {
					// Parse all command-line options
					for ( int n = 1, c = ( -1 ), offset = ( -1 ) ; n < args.Length ; n++ ) {
						if ( args [ n ] [ 0 ] == '-' ) {
							if ( args [ n ].Length < 2 ) {
								Console.WriteLine("error: invalid arguments");
								Usage(Path.GetFileName(Environment.GetCommandLineArgs() [ 0 ]));
								return;
							}

							for ( c = 1, offset = n ; c < args [ n ].Length ; c++ ) {
								switch ( args [ n ] [ c ] ) {
									case 't':
										if ( args.Length <= ++offset ) {
											Console.WriteLine("error: insufficient option arguments");
											Usage(Path.GetFileName(Environment.GetCommandLineArgs() [ 0 ]));
											return;
										}

										typeFiles = args [ offset ].Split(',');
										break;
									case 'E':
										if ( args.Length <= ++offset ) {
											Console.WriteLine("error: insufficient option arguments");
											Usage(Path.GetFileName(Environment.GetCommandLineArgs() [ 0 ]));
											return;
										}

										enumFiles = args [ offset ].Split(',');
										break;
									case 's':
										splitFiles = true;
										break;
									case 'h':
										Usage(Path.GetFileName(Environment.GetCommandLineArgs() [ 0 ]));
										break;
									case 'o':
										if ( !splitFiles ) {
											if ( args.Length <= ++offset ) {
												Console.WriteLine("error: insufficient option arguments");
												Usage(Path.GetFileName(Environment.GetCommandLineArgs() [ 0 ]));
												return;
											}

											outFileName = args [ offset ];
											genFileName = false;
										} else {
											Console.WriteLine("error: invalid option argument");
											Usage(Path.GetFileName(Environment.GetCommandLineArgs() [ 0 ]));
											return;
										}

										break;
									case 'v':
										About();
										break;
									case 'V':
										verbose = true;
										break;
									default:
										Console.WriteLine("error: invalid options");
										Usage(Path.GetFileName(Environment.GetCommandLineArgs() [ 0 ]));
										return;
								}
							}

							if ( offset > n )
								n++;
						}
					}
				}

				#endregion

				Console.WriteLine();

				try {
					int f = ( -1 );
					DateTime timer = DateTime.MinValue;

					if ( splitFiles ) {
						// If the -s flag was given then generate
						XmlDocument [ ] specXmlFiles = new XmlDocument [ specFiles.Length ];
						XmlDocument [ ] typeXmlFiles = null, enumXmlFiles = null;

						for ( f = 0 ; f < specFiles.Length ; f++ ) {
							specXmlFiles [ f ] = new XmlDocument();

							specXmlFiles [ f ].AppendChild(specXmlFiles [ f ].CreateXmlDeclaration("1.0", null, "yes"));
							specXmlFiles [ f ].AppendChild(specXmlFiles [ f ].CreateElement("spec"));
							specXmlFiles [ f ].DocumentElement.SetAttribute("timestamp", DateTime.UtcNow.ToBinary().ToString());
						}

						if ( ( typeFiles != null ) && ( typeFiles.Length > 0 ) ) {
							typeXmlFiles = new XmlDocument [ typeFiles.Length ];

							for ( f = 0 ; f < typeFiles.Length ; f++ ) {
								timer = DateTime.UtcNow;

								if ( verbose )
									Console.Write("Parsing typemap '{0}'... ", typeFiles [ f ]);

								typeXmlFiles [ f ] = new XmlDocument();

								typeXmlFiles [ f ].AppendChild(typeXmlFiles [ f ].CreateXmlDeclaration("1.0", null, "yes"));
								typeXmlFiles [ f ].AppendChild(typeXmlFiles [ f ].CreateElement("spec"));
								typeXmlFiles [ f ].DocumentElement.SetAttribute("timestamp", DateTime.UtcNow.ToBinary().ToString());
								CreateTypeMap(typeXmlFiles [ f ], GetFileLines(typeFiles [ f ]));

								if ( verbose )
									Console.WriteLine("({0} ms)", ( DateTime.UtcNow - timer ).TotalMilliseconds);
							}
						}

						if ( ( enumFiles != null ) && ( enumFiles.Length > 0 ) ) {
							enumXmlFiles = new XmlDocument [ enumFiles.Length ];

							for ( f = 0 ; f < enumFiles.Length ; f++ ) {
								timer = DateTime.UtcNow;

								if ( verbose )
									Console.Write("Parsing enumeration '{0}'... ", enumFiles [ f ]);

								enumXmlFiles [ f ] = new XmlDocument();

								enumXmlFiles [ f ].AppendChild(enumXmlFiles [ f ].CreateXmlDeclaration("1.0", null, "yes"));
								enumXmlFiles [ f ].AppendChild(enumXmlFiles [ f ].CreateElement("spec"));
								enumXmlFiles [ f ].DocumentElement.SetAttribute("timestamp", DateTime.UtcNow.ToBinary().ToString());
								CreateEnum(enumXmlFiles [ f ], GetFileLines(enumFiles [ f ]), typeXmlFiles);

								if ( verbose )
									Console.WriteLine("({0} ms)", ( DateTime.UtcNow - timer ).TotalMilliseconds);
							}
						}

						if ( verbose )
							Console.WriteLine("Linking files...");

						XmlElement [ ] linkElements = new XmlElement [ ] { null, null };

						if ( ( typeFiles != null ) && ( typeFiles.Length > 0 ) ) {
							for ( f = 0 ; f < typeFiles.Length ; f++ ) {
								if ( _patterns [ 1 ].IsMatch(typeFiles [ f ]) ) {
									typeFiles [ f ] = Path.GetFileNameWithoutExtension(( new Uri(typeFiles [ f ]) ).AbsolutePath);
								} else {
									typeFiles [ f ] = Path.GetFileNameWithoutExtension(typeFiles [ f ]);
								}

								if ( File.Exists(typeFiles [ f ] + "-tm.xml") ) {
									for ( int n = 1 ; n < ( int.MaxValue - 1 ) ; n++ ) {
										if ( !File.Exists(string.Format("{0}({1})-tm.xml", typeFiles [ f ], n)) ) {
											typeFiles [ f ] = string.Format("{0}({1})-tm.xml", typeFiles [ f ], n);
											break;
										}
									}
								} else {
									typeFiles [ f ] += "-tm.xml";
								}

								for ( int s = 0 ; s < specXmlFiles.Length ; s++ ) {
									linkElements [ 0 ] = specXmlFiles [ s ].CreateElement("link");

									linkElements [ 0 ].SetAttribute("type", "text/xml");
									linkElements [ 0 ].SetAttribute("rel", "typemap");
									linkElements [ 0 ].SetAttribute("href", typeFiles [ f ]);
									specXmlFiles [ s ].DocumentElement.AppendChild(linkElements [ 0 ]);
								}


								typeXmlFiles [ f ].Save(CreateXmlTextWriter(typeFiles [ f ]));
								Console.WriteLine("Saved output to '{0}'.", typeFiles [ f ]);
							}
						}

						if ( ( enumFiles != null ) && ( enumFiles.Length > 0 ) ) {
							for ( f = 0 ; f < enumFiles.Length ; f++ ) {
								if ( _patterns [ 1 ].IsMatch(enumFiles [ f ]) ) {
									enumFiles [ f ] = Path.GetFileNameWithoutExtension(( new Uri(enumFiles [ f ]) ).AbsolutePath);
								} else {
									enumFiles [ f ] = Path.GetFileNameWithoutExtension(enumFiles [ f ]);
								}

								if ( File.Exists(enumFiles [ f ] + "-enum.xml") ) {
									for ( int n = 1 ; n < ( int.MaxValue - 1 ) ; n++ ) {
										if ( !File.Exists(string.Format("{0}({1})-enum.xml", enumFiles [ f ], n)) ) {
											enumFiles [ f ] = string.Format("{0}({1})-enum.xml", enumFiles [ f ], n);
											break;
										}
									}
								} else {
									enumFiles [ f ] += "-enum.xml";
								}

								for ( int s = 0 ; s < specXmlFiles.Length ; s++ ) {
									linkElements [ 1 ] = specXmlFiles [ s ].CreateElement("link");

									linkElements [ 1 ].SetAttribute("type", "text/xml");
									linkElements [ 1 ].SetAttribute("rel", "enumeration");
									linkElements [ 1 ].SetAttribute("href", enumFiles [ f ]);
									specXmlFiles [ s ].DocumentElement.AppendChild(linkElements [ 1 ]);
								}

								enumXmlFiles [ f ].Save(CreateXmlTextWriter(enumFiles [ f ]));
								Console.WriteLine("Saved output to '{0}'.", enumFiles [ f ]);
							}
						}

						for ( f = 0 ; f < specXmlFiles.Length ; f++ ) {
							timer = DateTime.UtcNow;

							if ( verbose )
								Console.Write("Parsing specification '{0}'... ", specFiles [ f ]);

							using ( StringReader reader = new StringReader(GetFile(specFiles [ f ])) ) {
								Match match = null;
								string s = null; List<string> cdata = new List<string>();

								while ( ( s = reader.ReadLine() ) != null ) {
									if ( ( s.Length < 1 ) || ( s [ 0 ] == '#' ) || s.StartsWith("passthru:") )
										continue;

									if ( _patterns [ 2 ].IsMatch(s) ) {
										CreateDecl(specXmlFiles [ f ], s);
									} else if ( ( match = _patterns [ 3 ].Match(s) ) != Match.Empty ) {
										while ( ( s = reader.ReadLine() ) != string.Empty )
											cdata.Add(s);

										CreateFunc(specXmlFiles [ f ], cdata.ToArray(), match.Groups [ 1 ].Captures [ 0 ].Value);
										cdata.Clear();
									}
								}
							}

							if ( verbose )
								Console.WriteLine("({0} ms)", ( DateTime.UtcNow - timer ).TotalMilliseconds);

							if ( _patterns [ 1 ].IsMatch(specFiles [ f ]) ) {
								specFiles [ f ] = Path.GetFileNameWithoutExtension(( new Uri(specFiles [ f ]) ).AbsolutePath);
							} else {
								specFiles [ f ] = Path.GetFileNameWithoutExtension(specFiles [ f ]);
							}

							if ( File.Exists(specFiles [ f ] + ".xml") ) {
								for ( int n = 1 ; n < ( int.MaxValue - 1 ) ; n++ ) {
									if ( !File.Exists(string.Format("{0}({1}).xml", specFiles [ f ], n)) ) {
										specFiles [ f ] = string.Format("{0}({1}).xml", specFiles [ f ], n);
										break;
									}
								}
							} else {
								specFiles [ f ] += ".xml";
							}

							specXmlFiles [ f ].Save(CreateXmlTextWriter(specFiles [ f ]));
							Console.WriteLine("Saved output to '{0}'.", specFiles [ f ]);
						}
					} else {
						XmlDocument xmlFile = new XmlDocument();

						xmlFile.AppendChild(xmlFile.CreateXmlDeclaration("1.0", null, "yes"));
						xmlFile.AppendChild(xmlFile.CreateElement("spec"));
						xmlFile.DocumentElement.SetAttribute("timestamp", DateTime.UtcNow.ToBinary().ToString());

						for ( f = 0 ; f < specFiles.Length ; f++ ) {
							timer = DateTime.UtcNow;

							if ( verbose )
								Console.Write("Parsing specification '{0}'... ", specFiles [ f ]);

							using ( StringReader reader = new StringReader(GetFile(specFiles [ f ])) ) {
								Match match = null;
								string s = null; List<string> cdata = new List<string>();

								while ( ( s = reader.ReadLine() ) != null ) {
									if ( ( s.Length < 1 ) || ( s [ 0 ] == '#' ) || s.StartsWith("passthru:") )
										continue;

									if ( _patterns [ 2 ].IsMatch(s) ) {
										CreateDecl(xmlFile, s);
									} else if ( ( match = _patterns [ 3 ].Match(s) ) != Match.Empty ) {
										while ( ( s = reader.ReadLine() ) != string.Empty )
											cdata.Add(s);

										CreateFunc(xmlFile, cdata.ToArray(), match.Groups [ 1 ].Captures [ 0 ].Value);
										cdata.Clear();
									}
								}
							}

							if ( verbose )
								Console.WriteLine("({0} ms)", ( DateTime.UtcNow - timer ).TotalMilliseconds);
						}

						if ( ( typeFiles != null ) && ( typeFiles.Length > 0 ) ) {
							for ( f = 0 ; f < typeFiles.Length ; f++ ) {
								timer = DateTime.UtcNow;

								if ( verbose )
									Console.Write("Parsing typemap '{0}'... ", typeFiles [ f ]);

								CreateTypeMap(xmlFile, GetFileLines(typeFiles [ f ]));

								if ( verbose )
									Console.WriteLine("({0} ms)", ( DateTime.UtcNow - timer ).TotalMilliseconds);
							}
						}

						if ( ( enumFiles != null ) && ( enumFiles.Length > 0 ) ) {
							for ( f = 0 ; f < specFiles.Length ; f++ ) {
								timer = DateTime.UtcNow;

								if ( verbose )
									Console.Write("Parsing enumeration '{0}'... ", enumFiles [ f ]);

								CreateEnum(xmlFile, GetFileLines(enumFiles [ f ]), new XmlDocument [ ] { xmlFile });

								if ( verbose )
									Console.WriteLine("({0} ms)", ( DateTime.UtcNow - timer ).TotalMilliseconds);
							}
						}

						if ( genFileName || string.IsNullOrEmpty(outFileName) ) {
							if ( File.Exists(outFileName + ".xml") ) {
								for ( int n = 1 ; n < ( int.MaxValue - 1 ) ; n++ ) {
									if ( !File.Exists(string.Format("{0}-({1}).xml", outFileName, n)) ) {
										outFileName = string.Format("{0}-({1}).xml", outFileName, n);
										break;
									}
								}
							} else {
								outFileName += ".xml";
							}
						}

						xmlFile.Save(CreateXmlTextWriter(outFileName));
						Console.WriteLine("Saved output to '{0}'.", outFileName);
					}
				} catch ( Exception exception ) {
					Console.WriteLine();
					Console.WriteLine(exception.GetType().FullName);
					Console.WriteLine("Message: {0}", exception.Message);
					Console.WriteLine("Source:  {0}", exception.Source);
					Console.WriteLine("Target:  {0}", exception.TargetSite);
					Console.WriteLine("Stack Trace:");
					Console.WriteLine(exception.StackTrace);
				}
			} finally {
				Console.WriteLine();
			}
		}
	}
}
